<?php
/**
 * CHANGELOG:
 *  2010-06-21: Added resolving of @imports
 *  2010-06-14: Added CSS variables support (http://disruptive-innovations.com/zoo/cssvariables/)
 *              Be aware that this they are completely unsupported by browsers so as soon as you
 *              turn off the combining of stylesheets, all variables _will_ break.
 */
class Scrinker_CSS extends Scrinker
{
	const RESOLVE_IMPORTS     =  4;
	const SPRITIFY            =  8;
	const PARSE_CSS_VARIABLES = 16;
	const PARSE_EXTENDED_CSS  = 32;

	private static $combined_rules = array(
		'background'    => array('background-color', 'background-image', 'background-position', 'background-repeat'),
		'border'        => array('border-width', 'border-style', 'border-color'),
		'border-top'    => array('border-top-width', 'border-top-style', 'border-top-color'),
		'border-right'  => array('border-right-width', 'border-right-style', 'border-right-color'),
		'border-bottom' => array('border-bottom-width', 'border-bottom-style', 'border-bottom-color'),
		'border-left'   => array('border-left-width', 'border-left-style', 'border-left-color'),
		'list-style'    => array('list-style-type', 'list-style-position', 'list-style-image'),
		'margin'        => array('margin-top', 'margin-right', 'margin-bottom', 'margin-left'),
		'padding'       => array('padding-top', 'padding-right', 'padding-bottom', 'padding-left'),
		'outline'       => array('outline-width', 'outline-style', 'outline-color'),
	);
	private static $expandos = array(
		'attributes' => array(
			'border-radius'              => array('-moz-border-radius', '-webkit-border-radius'),
			'border-top-left-radius'     => array('-moz-border-radius-topleft', '-webkit-border-top-left-radius'),
			'border-top-right-radius'    => array('-moz-border-radius-topright', '-webkit-border-top-right-radius'),
			'border-bottom-left-radius'  => array('-moz-border-radius-bottomleft', '-webkit-border-bottom-left-radius'),
			'border-bottom-right-radius' => array('-moz-border-radius-bottomright', '-webkit-border-bottom-right-radius'),
			'box-shadow'                 => array('-moz-box-shadow', '-webkit-box-shadow'),
		),
		'values' => array(
		),
	);
	private $variables = array();

	public function __construct($options = array())
	{
		if (empty($options['template']))
		 	$options['template'] = '<link href="%s" rel="stylesheet" type="text/css" />';
		parent::__construct($options);
	}

	private function parse_rules($string)
	{
		$string = trim($string);

		$rules = array();
		while (!empty($string))
		{
			@list($attribute, $string) = explode(':', $string, 2);

			$value = '';
			do
			{
				@list($v, $string) = explode(';', $string, 2);
				$value .= $v.';';
			}
			while (strpos($string, ';') < strpos($string, ':')); // Assure right order
			$rules[trim($attribute)] = trim(rtrim($value, ';'));
		}
		return $rules;
	}

	private function replace($string, $replaces)
	{
		return str_replace(array_keys($replaces), array_values($replaces), $string);
	}

	protected function on_combine($combined, $options, $flags)
	{
		$result = parent::on_combine($combined, $options, $flags);

		if ($flags & self::RESOLVE_IMPORTS)
			$result = $this->resolve_imports($result, dirname($options['filename']));

		if ($flags & self::PARSE_EXTENDED_CSS)
		{
			// Yet to implement
		}

		if ($flags & self::PARSE_CSS_VARIABLES)
		{
			$this->variables = array_merge($this->variables, $this->parse_variables($result));
			$result = $this->replace_variables($result, $this->variables);
		}

		if ($flags & self::SPRITIFY)
			Sprites::GetInstance()->parse($result, dirname($options['filename']));

		return $result;
	}

	private function resolve_imports($string, $basepath)
	{
		$replaces = array();

		preg_match_all('/@import\s+(?:url\()?(")?([^"]+?)\\1(?:\))?(\s+[^;]+)?;/', $string, $matches, PREG_SET_ORDER);
		foreach ($matches as $match)
		{
			$import_filename = $basepath.'/'.$match[2];
			$import = file_get_contents($import_filename);
			$import = $this->resolve_imports($import, dirname($import_filename));

			$replaces[$match[0]] = '/** Imported file: "'.$import_filename.'" **/'."\n".$import;
		}

		return $this->replace($string, $replaces);
	}

	private function parse_variables(&$string)
	{
		$variables = array();

		preg_match_all('/@variables\s+(\w+)?\s*\{([^\}]+)}\s*/', $string, $matches, PREG_SET_ORDER);
		foreach ($matches as $match)
		{
			if (!empty($match[1]) and $match[1]!='screen')
				throw new Scrinker_Exception('CSS Variables: Other media types than "screen" can not be supported');

			$variables = array_merge($variables, $this->parse_rules($match[2]));
			$string = str_replace($match[0], '', $string);
		}
		return $variables;
	}

	private function replace_variables($string, $variables)
	{
		$replaces = array();
		foreach ($variables as $key => $value)
			$replaces['var('.$key.')'] = $value;
		return $this->replace($string, $replaces);
	}

	protected function after_combine($string, $options, $flags)
	{
		if ($flags & self::SPRITIFY and !empty($options['sprite_filename']))
		{
			$replaces = Sprites::GetInstance()->engage(
				$options['sprite_filename'],
				isset($options['sprite_url']) ? $options['sprite_url'] : null
			);
			$string = $this->replace($string, $replaces);

			if ($flags & self::MINIMIZE)
				Minimizer::Minimize_Image($options['sprite_filename']);
		}


		if ($flags & self::MINIMIZE)
		{
			// Step 1: Apply cssmin
			$string = cssmin::minify($string);

			// Remove duplicate rules
			$replaces = array();

			preg_match_all('/\{(.*?)\}/', $string, $matches, PREG_SET_ORDER);
			foreach ($matches as $match)
			{
				$rules = $this->parse_rules($match[1]);

				// Possible to combine certain rules?
				foreach (self::$combined_rules as $combined => $separate)
					if (count(array_intersect($separate, array_keys($rules))) == count($separate))
					{
						$rules[$combined] = array();
						foreach ($separate as $key)
						{
							array_push($rules[$combined], $rules[$key]);
							unset($rules[$key]);
						}
						$rules[$combined] = implode(' ', $rules[$combined]);
					}

				// Any attributes to adapt to different browser syntaxes?
				foreach (self::$expandos['attributes'] as $attribute => $expanded)
					if (array_key_exists($attribute, $rules) and count(array_intersect($expanded, array_keys($rules)))==0)
						foreach ($expanded as $attr)
							$rules[$attr] = $rules[$attribute];

				// Write back the rules
				$replaces[$match[0]] = '{'.implode(';', array_map(create_function('$a, $v', 'return $a.":".$v;'), array_keys($rules), array_values($rules))).'}';
			}
			$string = $this->replace($string, $replaces);

			// Step 3: strtolower( all hex color codes )
			$string = preg_replace_callback('/\#[0-9A-F]+/', create_function('$a', 'return strtolower($a[0]);'), $string);

		}

		if ($flags & self::KEEP_LINEBREAKS)
			$string = rtrim(str_replace('}', '}'."\n", $string));

		return parent::after_combine($string, $options, $flags);
	}
}
